﻿/*
AwpAdmin, an admin tool for Battlefield 3
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3admin.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Net.Battlefield3;
using System.Globalization;
using Microsoft.VisualBasic.FileIO;
using System.Runtime.Remoting.Lifetime;

namespace AwpAdminTool
{
    /// <summary>
    /// The main AwpAdmin API.
    /// </summary>
    public class AwpAdmin
        : MarshalByRefObject,
        IDisposable
    {
        static CultureInfo c = CultureInfo.GetCultureInfo("en-US");


        RconClient _Client = new RconClient();
        /// <summary>
        /// Gets the parent <see cref="RconClient"/> of the <see cref="AwpAdmin"/>.
        /// </summary>
        /// <value>The parent <see cref="RconClient"/>.</value>
        public RconClient Client { get { return _Client; } }

        PluginManager _Plugins;
        /// <summary>
        /// Gets the <see cref="PluginManager"/> of the <see cref="AwpAdmin"/>.
        /// </summary>
        /// <value>The <see cref="PluginManager"/> of the <see cref="AwpAdmin"/>.</value>
        public PluginManager Plugins { get { return _Plugins; } }

        #region AwpAdmin <-> RconClient
        //these are so operations, like Player.GetAdmin() don't require an AwpAdmin to be supplied.

        //at the moment, the only operations are for rconclient -> awpadmin, but consider adding 2nd dict later for other way
        static Dictionary<RconClientBase, AwpAdmin> AdminTracker = new Dictionary<RconClientBase, AwpAdmin>();

        internal static AwpAdmin GetAwpAdmin(RconClientBase client)
        {
            AwpAdmin admin;
            AdminTracker.TryGetValue(client, out admin); //eliminates need for ContainsKey

            return admin; //an AwpAdmin or null
        }

        internal static void SetAwpAdmin(RconClient client, AwpAdmin admin)
        {
            AdminTracker[client] = admin;
        }
        #endregion

        #region Settings-related members (main and admin settings)
        //the xml namespace all of the settings files will use
        static internal XNamespace XmlNamespace = "http://bf3admin.codeplex.com/";
        internal XElement MainSettings;
        internal XElement AdminSettings; //for memory convenience, do this here
        internal XElement AdminSettingsCopy { get { return new XElement(AdminSettings); } }

        #region Properties
        string _WorkingDirectory;

        /// <summary>
        /// Gets the absolute path to the main settings directory.
        /// </summary>
        /// <value>The absolute path the to main settings directory.</value>
        /// <remarks>
        /// The location of some settings, such as admins, can be changed to a different directory.<para />
        /// This appends the trailing slash to the path.
        /// </remarks>
        public string WorkingDirectory
        {
            get
            { 
                string p = Path.GetFullPath(_WorkingDirectory) + "\\";

                return Path.DirectorySeparatorChar == '\\' ? p : p.Replace('\\', Path.DirectorySeparatorChar);
            }
            internal set { _WorkingDirectory = value; } //will only be set in constructor
        }

        /// <summary>
        /// Gets or sets the location of the admin file.
        /// </summary>
        /// <value>
        /// The location of the admin file relative to <see cref="WorkingDirectory"/> 
        /// or an absolute path to the admin file.
        /// </value>
        public string AdminsFile
        {
            get
            {
                string p = MainSettings.Element(XmlNamespace + "adminsFile").Value;

                return Path.DirectorySeparatorChar == '\\' ? p : p.Replace('\\', Path.DirectorySeparatorChar);
            }
            set
            {
                MainSettings.Element(XmlNamespace + "adminsFile").Value = value; SaveMainSettings();

                LoadAdminSettings();
            }
        }

        //for now, dont allow it to change because it's kinda complicated to unload and load plugins efficiently
        string _PluginDirectory;
        /// <summary>
        /// Gets the directory where <see cref="Plugin">Plugins</see> are located.
        /// </summary>
        /// <value>The directory where <see cref="Plugin">Plugins</see> are located.</value>
        public string PluginDirectory
        {
            get{ return _PluginDirectory; } //dirsepchar is checked when settings are loaded
            internal set
            {
                MainSettings.Element(XmlNamespace + "pluginDirectory").Value = value; SaveMainSettings();
            }
        }
        #endregion

        #region Main Settings
        internal void SaveMainSettings()
        {
            MainSettings.Document.Save(WorkingDirectory + "settings.xml");
        }

        //this will also ensure the settings file has the minimum required settings
        void LoadMainSettings()
        {
            if (!File.Exists(WorkingDirectory + "settings.xml"))
            {
                //generate a new xml file and update as needed
                XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

                XDocument x = new XDocument(
                    new XElement(XmlNamespace + "awpAdmin",
                        new XAttribute(XNamespace.Xmlns + "xsi", xsi),
                        new XAttribute(xsi + "schemaLocation", "http://bf3admin.codeplex.com/ MainSettingsSchema.xsd"),

                        new XElement(XmlNamespace + "adminsFile", "admins.xml"),
                        new XElement(XmlNamespace + "commandsFile", "commands.xml"),
                        new XElement(XmlNamespace + "pluginDirectory", "plugins\\"),

                        new XElement(XmlNamespace + "server",
                            new XElement(XmlNamespace + "address", ""),
                            new XElement(XmlNamespace + "port", "0"),
                            new XElement(XmlNamespace + "pass", "")
                            ),
                        new XElement(XmlNamespace + "plugins",
                            new XElement(XmlNamespace + "plugin", "awp_AdminCommands")
                            )
                        )
                    );

                MainSettings = x.Root;
                SaveMainSettings();

                //copy xsd
                File.Copy("MainSettingsSchema.xsd", WorkingDirectory + "MainSettingsSchema.xsd", true);
            }
            else
            {
                //load xml if not done
                MainSettings = XDocument.Load(WorkingDirectory + "settings.xml").Root;

                //if it fails to validate, have UI deal with the unhandled exception
                XmlValidator.ValidateXml(MainSettings, "MainSettingsSchema.xsd", false);
            }

            //set plugindir
            _PluginDirectory = MainSettings.Element(XmlNamespace + "pluginDirectory").Value;
            if (Path.DirectorySeparatorChar != '\\') _PluginDirectory =
                _PluginDirectory.Replace('\\', Path.DirectorySeparatorChar);
        }
        #endregion

        #region Admin Settings
        //note: atm, no save method is needed because the Admin class has a save

        internal void LoadAdminSettings()
        {
            //get the absolute location of the admins.xml
            string fileLocation = Path.IsPathRooted(AdminsFile) ? AdminsFile : WorkingDirectory + AdminsFile;

            if (!File.Exists(fileLocation))
            {
                //generate a new xml file
                XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

                XDocument x = new XDocument(
                    new XElement(XmlNamespace + "admins",
                        new XAttribute(XNamespace.Xmlns + "xsi", xsi),
                        new XAttribute(xsi + "schemaLocation", "http://bf3admin.codeplex.com/ AdminSchema.xsd")
                        )
                    );

                AdminSettings = x.Root;
                x.Save(fileLocation);

                //copy xsd
                File.Copy("AdminSchema.xsd", Path.GetDirectoryName(fileLocation) + "\\AdminSchema.xsd", true);
            }
            else
            {
                AdminSettings = XDocument.Load(fileLocation).Root;

                //if it fails to validate, have UI deal with the unhandled exception
                XmlValidator.ValidateXml(AdminSettings, "AdminSchema.xsd", false);
            }
        }
        #endregion

        #endregion

        #region Commands
        //registered commands show up in /help, /commands, have /help <command> text, and raise the Command event.

        Dictionary<string, string> commandHelpText = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); //also serves to show what commands are registered
        XElement CommandSettings;
        XElement CommandSettingsCopy { get { return new XElement(CommandSettings); } }

        /// <summary>
        /// Gets or sets the location of the commands file.
        /// </summary>
        /// <value>
        /// The location of the commands file relative to <see cref="WorkingDirectory"/> 
        /// or an absolute path to the commands file.
        /// </value>
        public string CommandsFile
        {
            get
            {
                string p = MainSettings.Element(XmlNamespace + "commandsFile").Value;

                return Path.DirectorySeparatorChar == '\\' ? p : p.Replace('\\', Path.DirectorySeparatorChar);
            }
            set
            {
                MainSettings.Element(XmlNamespace + "commandsFile").Value = value; SaveMainSettings();

                LoadCommandSettings();
            }
        }

        internal void LoadCommandSettings()
        {
            //get the absolute location of the admins.xml
            string fileLocation = Path.IsPathRooted(CommandsFile) ? CommandsFile : WorkingDirectory + CommandsFile;

            if (!File.Exists(fileLocation))
            {
                //generate a new xml file
                XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

                XDocument x = new XDocument(
                    new XElement(XmlNamespace + "commands",
                        new XAttribute(XNamespace.Xmlns + "xsi", xsi),
                        new XAttribute(xsi + "schemaLocation", "http://bf3admin.codeplex.com/ CommandsSchema.xsd")
                        )
                    );

                CommandSettings = x.Root;
                x.Save(fileLocation);

                //copy xsd
                File.Copy("CommandsSchema.xsd", Path.GetDirectoryName(fileLocation) + "\\CommandsSchema.xsd", true);
            }
            else
            {
                CommandSettings = XDocument.Load(fileLocation).Root;

                //if it fails to validate, have UI deal with the unhandled exception
                XmlValidator.ValidateXml(CommandSettings, "CommandsSchema.xsd", false);
            }

        }

        void SaveCommandSettings()
        {
            string fileLocation = Path.IsPathRooted(CommandsFile) ? CommandsFile : WorkingDirectory + CommandsFile;
            CommandSettings.Document.Save(fileLocation);
        }

        /// <summary>
        /// Registers a new command.
        /// </summary>
        /// <param name="command">The command that is being registered.</param>
        /// <param name="helpText">The help text that will show up in /help &lt;command&gt;.</param>
        public void RegisterCommand(string command, string helpText)
        {
            //dont check to see if a command exists because the help text can be updated
            commandHelpText[command] = helpText;
        }

        /// <summary>
        /// Unregisters a previously registered command.
        /// </summary>
        /// <param name="command">The command that is being unregistered.</param>
        public void UnregisterCommand(string command)
        {
            commandHelpText.Remove(command);
        }

        /// <summary>
        /// Gets the access level for a command.
        /// </summary>
        /// <param name="command">The target command.</param>
        /// <returns>The access level of the target command.</returns>
        /// <remarks>
        /// By default, commands have an access level of 2147483648. Commands with access levels of -1 are disabled.
        /// </remarks>
        public int GetCommandAccessLevel(string command)
        {
            foreach (var e in CommandSettingsCopy.Elements(XmlNamespace + "command"))
                if (string.Equals(e.Attribute("name").Value, command, StringComparison.OrdinalIgnoreCase))
                    return Convert.ToInt32(e.Attribute("rank").Value, c);
                

            CreateCommandXml(command);
            return Int32.MaxValue; //default
        }

        /// <summary>
        /// Sets the access level for a command.
        /// </summary>
        /// <param name="command">The target command.</param>
        /// <param name="rank">The new access level of the command.</param>
        /// <remarks>
        /// By default, commands have an access level of 2147483648. Commands with access levels of -1 are disabled.
        /// </remarks>
        public void SetCommandAccessLevel(string command, int rank)
        {
            //validate rank. note that rank cannot be greater than maxvalue, so dont check for it
            if (rank < -1) throw new ArgumentOutOfRangeException("rank", "rank must not be less than -1.");

            XElement commandXml = GetCommandXml(command);

            if (commandXml != null)
                commandXml.Attribute("rank").Value = rank.ToString(c);
            else
                CreateCommandXml(command, rank);
        }

        XElement GetCommandXml(string command)
        {
            //lock to ensure elements arent added/removed during enumeration
            lock (CommandSettings)
                foreach (var e in CommandSettings.Elements(XmlNamespace + "command"))
                    if (e.Attribute("name").Value == command) return e;
            return null;
        }

        void CreateCommandXml(string command, int rank = Int32.MaxValue)
        {
            //since this is private, assume the xml doc was checked for the command
            //lock to ensure element isnt added during enumeration
            lock (CommandSettings)
                CommandSettings.Add(new XElement(XmlNamespace + "command",
                    new XAttribute("name", command),
                    new XAttribute("rank", rank)
                    ));

            SaveCommandSettings();
        }
        #endregion

        //these events are not optimized for multiple appdomains because the UIs wont be in different appdomains
        //events are internal because plugins should not use them
        #region Events
        /// <summary>
        /// Occurs when the <see cref="AwpAdmin"/> receives a command and the <see cref="Player">Player's</see>
        /// rank is less than or equal to the command's access level.
        /// </summary>
        internal event EventHandler<CommandEventArgs> Command;
        /// <summary>
        /// Raises the <see cref="Command"/> event of the <see cref="AwpAdmin"/>.
        /// </summary>
        /// <param name="e">A <see cref="CommandEventArgs"/> that contains the event data.</param>
        protected virtual void OnCommand(CommandEventArgs e)
        {
            if (Command != null) Command(this, e);

            //plugins
            foreach (var p in _Plugins.Plugins)
                try
                {
                    p.Value.OnCommand(e);
                }
                catch (Exception ex)
                {
                    //TODO: event

                    //unload plugin
                    Plugins.UnloadPlugin(p.Key);
                }
                
        }
        #endregion

        #region Ctors
        /// <summary>
        /// Creates a new <see cref="AwpAdmin"/>.
        /// </summary>
        public AwpAdmin()
            : this("settings")
        {

        }

        /// <summary>
        /// Creates a new <see cref="AwpAdmin"/>, with settings based out of the specified directory.
        /// </summary>
        /// <param name="workingDirectory">The directory where settings will be contained.</param>
        public AwpAdmin(string workingDirectory)
        {
            SetAwpAdmin(Client, this); //add to admin tracker

            if (!Directory.Exists(workingDirectory)) Directory.CreateDirectory(workingDirectory);

            WorkingDirectory = workingDirectory;

            LoadMainSettings();
            LoadAdminSettings();
            LoadCommandSettings();

            _Plugins = new PluginManager(this);

            //events
            Client.PlayerChat += new EventHandler<PlayerChatEventArgs>(Client_PlayerChat);
            Client.Connected += new EventHandler(Client_Connected);

            //set rconclient server
            Client.Address = MainSettings.Element(XmlNamespace + "server").Element(XmlNamespace + "address").Value;
            Client.Port = Convert.ToInt32(MainSettings.Element(XmlNamespace + "server").Element(XmlNamespace + "port").Value, c);
        }
        #endregion

        #region RconClient Event Handlers
        void Client_PlayerChat(object sender, PlayerChatEventArgs e)
        {
            //length must be >1 because / isnt a command :P
            if (e.Message.Length > 1 && e.Message.IndexOf('/') == 0)
            {
                //used to see if there are params and, if there are, where the cmd ends
                int firstSpace = e.Message.IndexOf(' ');

                string command = firstSpace != -1 ? e.Message.Substring(1, firstSpace - 1) : e.Message.Substring(1);

                //ensure command is registered
                if (!commandHelpText.ContainsKey(command)) return;

                int accessLevel = GetCommandAccessLevel(command);

                //dont raise the event if the player doesn't have access
                if (accessLevel > e.Player.GetAdmin().Rank) return;

                if (firstSpace == -1 || //no parameters
                    command.Length + 2 == e.Message.Length || //message is "/command "
                    command.Length + 1 == e.Message.Trim().Length) //rest is spaces
                {
                    OnCommand(new CommandEventArgs(
                        e.Player,
                        command,
                        new System.Collections.ObjectModel.ReadOnlyCollection<string>(new List<string>()),
                        string.Empty,
                        accessLevel
                        ));
                }
                else //has params
                {
                    string fullParams = e.Message.Substring(firstSpace + 1).TrimStart(' ');
                    TextFieldParser parser = new TextFieldParser(new StringReader(fullParams))
                    {
                        HasFieldsEnclosedInQuotes = true,
                        Delimiters = new string[] { " ", "\t" }
                    };
                    string[] parameters = parser.ReadFields();
                    OnCommand(new CommandEventArgs(
                        e.Player,
                        command,
                        new System.Collections.ObjectModel.ReadOnlyCollection<string>(parameters),
                        fullParams,
                        accessLevel
                        ));
                }
            }
        }

        void Client_Connected(object sender, EventArgs e)
        {
            Client.LogOn(MainSettings.Element(XmlNamespace + "server").Element(XmlNamespace + "pass").Value, true);
        }
        #endregion

        #region IDisposable Members
        private bool disposed = false;

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        /// <param name="disposing">Whether or not to release managed resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    _Client.Dispose();

                    //remove rconclient from dict
                    AdminTracker.Remove(_Client);
                }
            }

            disposed = true;
        }

        #endregion

        /// <summary>
        /// Initializes the lifetime lease of the <see cref="Plugin"/>.
        /// </summary>
        /// <returns>An initialized ILease that allows you to control the lifetime of the <see cref="Plugin"/>.</returns>
        public sealed override object InitializeLifetimeService() //sealed because this is important
        {
            ILease lease = base.InitializeLifetimeService() as ILease;
            if (lease.CurrentState == LeaseState.Initial)
                lease.InitialLeaseTime = TimeSpan.Zero;
            return lease;
        }
    }
}
